﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace TypeBuilderFx.Core
{
    /// <summary>Helper class regrouping helper methods manipulating types.</summary>
    public static class TypeHelper
    {
        /// <summary>
        /// Returns the value type represented as a <c>nullable</c> or <c>null</c> if the type isn't a value type nullable.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Type GetNullableUnderlyingType(Type type)
        {
            Type nullableTypeDefinition = typeof(Nullable<int>).GetGenericTypeDefinition();

            if (type.IsGenericType && type.GetGenericTypeDefinition() == nullableTypeDefinition)
            {   //  Type is a nullable type
                return type.GetGenericArguments()[0];
            }
            else
            {   //  Type isn't a nullable type
                return null;
            }
        }

        /// <summary>
        /// Returns the generic type definition of a factory of a given number of method parameters.
        /// The generic type definition is the type without its generic parameter.
        /// </summary>
        /// <param name="factoryMethodParameterCount"></param>
        /// <returns></returns>
        public static Type GetFactoryGenericTypeDefinition(int factoryMethodParameterCount)
        {
            switch (factoryMethodParameterCount)
            {
                case 0:
                    return typeof(IFactory<TypeEmitterBase<TypeEmitterOptions>>).GetGenericTypeDefinition();
                case 1:
                    return typeof(IFactory<TypeEmitterBase<TypeEmitterOptions>, TypeEmitterBase<TypeEmitterOptions>>).GetGenericTypeDefinition();
                case 2:
                    return typeof(IFactory<TypeEmitterBase<TypeEmitterOptions>, TypeEmitterBase<TypeEmitterOptions>, TypeEmitterBase<TypeEmitterOptions>>).GetGenericTypeDefinition();
                default:
                    throw new NotSupportedException("Factory with more than 2 parameters isn't supported yet.");
            }
        }

        /// <summary>Returns the list of interfaces exposed by a type.</summary>
        /// <param name="type">Type to introspect.</param>
        /// <param name="isCurrentIncluded">Includes <paramref name="type"/>.</param>
        /// <returns></returns>
        public static Type[] GetAllInterfaces(Type type, bool isCurrentIncluded)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            List<Type> interfaceTypeList = new List<Type>();

            if (isCurrentIncluded && type.IsInterface)
            {
                interfaceTypeList.Add(type);
            }
            GetAllInterfaces(type, interfaceTypeList);

            return interfaceTypeList.ToArray();
        }

        #region Type List Manipulation
        /// <summary>Concatenates two type lists into another list.</summary>
        /// <param name="typeList1"></param>
        /// <param name="typeList2"></param>
        /// <returns></returns>
        public static Type[] Concat(Type[] typeList1, Type[] typeList2)
        {
            Type[] superList = new Type[typeList1.Length + typeList2.Length];

            Array.Copy(typeList1, superList, typeList1.Length);
            Array.Copy(typeList2, 0, superList, typeList1.Length, typeList2.Length);

            return superList;
        }

        /// <summary>Concatenates a type list and a type into another list.</summary>
        /// <param name="typeList"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Type[] Concat(Type[] typeList, Type type)
        {
            Type[] superList = new Type[typeList.Length + 1];

            Array.Copy(typeList, superList, typeList.Length);
            superList[typeList.Length] = type;

            return superList;
        }
        #endregion

        #region Equivalence relations
        /// <summary>Returns <c>true</c> iif both collections are equivalent (same type).</summary>
        /// <param name="parameterList1"></param>
        /// <param name="parameterList2"></param>
        /// <returns></returns>
        public static bool AreEquivalent(ParameterInfo[] parameterList1, ParameterInfo[] parameterList2)
        {
            if (parameterList1 == null)
            {
                throw new ArgumentNullException("parameterList1");
            }
            if (parameterList2 == null)
            {
                throw new ArgumentNullException("parameterList2");
            }

            if (parameterList1.Length != parameterList2.Length)
            {
                return false;
            }
            for (int i = 0; i != parameterList1.Length; ++i)
            {
                if (!AreEquivalent(parameterList1[i], parameterList2[i]))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Returns <c>true</c> iif both parameter info are equivalent:  they both represent the same type, same parameter
        /// attribute (e.g. <c>in</c>, <c>out</c>, etc.) but they can be from different methods or different owning type.
        /// </summary>
        /// <param name="parameterInfo1"></param>
        /// <param name="parameterInfo2"></param>
        /// <returns></returns>
        public static bool AreEquivalent(ParameterInfo parameterInfo1, ParameterInfo parameterInfo2)
        {
            if (parameterInfo1 == null)
            {
                throw new ArgumentNullException("parameterInfo1");
            }
            if (parameterInfo2 == null)
            {
                throw new ArgumentNullException("parameterInfo2");
            }

            return parameterInfo1.ParameterType == parameterInfo2.ParameterType
                && parameterInfo1.Attributes == parameterInfo2.Attributes;
        }
        #endregion

        private static void GetAllInterfaces(Type type, List<Type> interfaceTypeList)
        {
            Type[] directInterfaceTypeList = Array.FindAll(
                type.GetInterfaces(),
                delegate(Type interfaceType) { return !interfaceTypeList.Contains(interfaceType); });

            interfaceTypeList.AddRange(directInterfaceTypeList);
            Array.ForEach(
                directInterfaceTypeList,
                delegate(Type interfaceType) { GetAllInterfaces(interfaceType, interfaceTypeList); });
        }
    }
}